SPB Git

spb/spboucher.ai Public

spboucher.ai — personal website of Simon-Pierre Boucher.

TypeScript 93.4% HTML 5.5% CSS 1%
10.4 KB · 309 lines tsx
Raw Blame History
1/*2  page.tsx3  spboucher.ai Web4  Author: Simon-Pierre Boucher5  Mail: contact@spboucher.ai6*/78import type { Metadata } from "next";9import Link from "next/link";10import { notFound } from "next/navigation";11import {12  ArrowLeft,13  Braces,14  CheckCircle2,15  Database,16  FileText,17  FlaskConical,18  Github,19  ScrollText,20} from "lucide-react";2122import { Badge } from "@/components/ui/badge";23import { buttonVariants } from "@/components/ui/button";24import { Reveal } from "@/components/reveal";25import { SectionHeading } from "@/components/section-heading";26import { getResearchPaper, allResearchPapers } from "@/lib/research";27import { researchDetails } from "@/lib/research-details";2829interface PageProps {30  params: Promise<{ slug: string }>;31}3233export function generateStaticParams() {34  return allResearchPapers35    .filter((p) => researchDetails.some((d) => d.slug === p.slug))36    .map((p) => ({ slug: p.slug }));37}3839export async function generateMetadata({40  params,41}: PageProps): Promise<Metadata> {42  const { slug } = await params;43  const paper = getResearchPaper(slug);44  const detail = researchDetails.find((d) => d.slug === slug);45  if (!paper || !detail) return {};46  return {47    title: `${paper.title} — ${paper.num}`,48    description: detail.hero.subheadline,49  };50}5152export default async function ResearchDetailPage({ params }: PageProps) {53  const { slug } = await params;54  const paper = getResearchPaper(slug);55  const detail = researchDetails.find((d) => d.slug === slug);56  if (!paper || !detail) notFound();5758  return (59    <article className="mx-auto max-w-4xl px-4 py-16 sm:px-6">60      <Reveal>61        <Link62          href="/research"63          className="inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-primary"64        >65          <ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />66          All research67        </Link>68      </Reveal>6970      {/* Header */}71      <Reveal className="mt-10">72        <header>73          <p className="flex flex-wrap items-center gap-3 font-mono text-[0.68rem] font-medium uppercase tracking-[0.2em] text-primary">74            <span aria-hidden="true" className="h-px w-6 bg-primary/60" />75            {paper.num}76            {paper.pages && (77              <span className="text-muted-foreground/80">· {paper.pages}</span>78            )}79            {paper.status && (80              <span className="text-muted-foreground/80">· {paper.status}</span>81            )}82          </p>83          <h1 className="mt-4 font-display text-3xl font-semibold leading-tight tracking-tight sm:text-[2.6rem] sm:leading-[1.12]">84            {paper.title}85          </h1>86          <p className="mt-5 max-w-2xl font-display text-lg font-medium leading-snug tracking-tight text-foreground/85 [font-style:italic] sm:text-xl">87            {detail.hero.headline}88          </p>89          <p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted-foreground">90            {detail.hero.subheadline}91          </p>92          <p className="mt-4 text-sm text-muted-foreground">93            Simon-Pierre Boucher —{" "}94            <a95              href="mailto:contact@spboucher.ai"96              className="text-primary hover:underline"97            >98              contact@spboucher.ai99            </a>100          </p>101          <div className="mt-6 flex flex-wrap gap-2">102            <a103              href={paper.pdf}104              target="_blank"105              rel="noopener noreferrer"106              className={buttonVariants({ size: "sm" })}107            >108              <FileText aria-hidden="true" />109              Paper PDF110            </a>111            {paper.repo && (112              <a113                href={paper.repo}114                target="_blank"115                rel="noopener noreferrer"116                className={buttonVariants({ variant: "outline", size: "sm" })}117              >118                <Github aria-hidden="true" />119                Code &amp; data repository120              </a>121            )}122            {paper.source && (123              <a124                href={paper.source}125                target="_blank"126                rel="noopener noreferrer"127                className={buttonVariants({ variant: "outline", size: "sm" })}128              >129                <Braces aria-hidden="true" />130                LaTeX source131              </a>132            )}133          </div>134        </header>135      </Reveal>136137      {/* Headline findings */}138      {detail.findings.length > 0 && (139        <Reveal className="mt-14">140          <div className="grid grid-cols-2 gap-x-8 gap-y-8 sm:grid-cols-3">141            {detail.findings.map((f) => (142              <div key={f.label} className="border-t-2 border-foreground pt-3">143                <p className="font-display text-3xl font-semibold tracking-tight sm:text-4xl">144                  {f.value}145                </p>146                <p className="mt-1.5 font-mono text-[0.66rem] uppercase leading-relaxed tracking-[0.12em] text-muted-foreground">147                  {f.label}148                </p>149              </div>150            ))}151          </div>152        </Reveal>153      )}154155      {/* Abstract */}156      <section className="mt-14">157        <Reveal>158          <SectionHeading title="Abstract" icon={ScrollText} />159        </Reveal>160        <Reveal delay={0.06}>161          <div className="mt-5 max-w-3xl space-y-4 border-l border-border pl-5 sm:pl-6">162            {detail.abstract.map((paragraph) => (163              <p164                key={paragraph.slice(0, 48)}165                className="text-sm leading-relaxed text-muted-foreground sm:text-[0.95rem]"166              >167                {paragraph}168              </p>169            ))}170          </div>171        </Reveal>172      </section>173174      {/* Contributions */}175      {detail.contributions.length > 0 && (176        <section className="mt-14">177          <Reveal>178            <SectionHeading title="Key Results" icon={FlaskConical} />179          </Reveal>180          <ol className="mt-6 space-y-5">181            {detail.contributions.map((c, i) => (182              <Reveal key={c.title} delay={i * 0.05}>183                <li className="flex items-start gap-4 border-b border-border/60 pb-5 last:border-0">184                  <span185                    aria-hidden="true"186                    className="pt-0.5 font-mono text-sm font-semibold text-primary"187                  >188                    {String(i + 1).padStart(2, "0")}189                  </span>190                  <div className="min-w-0">191                    <h3 className="font-display text-base font-semibold tracking-tight">192                      {c.title}193                    </h3>194                    <p className="mt-1 max-w-2xl text-sm leading-relaxed text-muted-foreground">195                      {c.description}196                    </p>197                  </div>198                </li>199              </Reveal>200            ))}201          </ol>202        </section>203      )}204205      {/* Data & methodology */}206      <div className="mt-14 grid gap-12 md:grid-cols-2 md:gap-10">207        {detail.data.length > 0 && (208          <section>209            <Reveal>210              <SectionHeading title="Data" icon={Database} />211            </Reveal>212            <div className="mt-5 space-y-4">213              {detail.data.map((d, i) => (214                <Reveal key={d.title} delay={i * 0.05}>215                  <div className="border-l-2 border-primary/50 pl-4">216                    <h3 className="text-sm font-semibold">{d.title}</h3>217                    <p className="mt-1 text-sm leading-relaxed text-muted-foreground">218                      {d.description}219                    </p>220                  </div>221                </Reveal>222              ))}223            </div>224          </section>225        )}226227        {detail.methodology.length > 0 && (228          <section>229            <Reveal>230              <SectionHeading title="Methodology" icon={FlaskConical} />231            </Reveal>232            <div className="mt-5 space-y-4">233              {detail.methodology.map((m, i) => (234                <Reveal key={m.title} delay={i * 0.05}>235                  <div className="border-l-2 border-border pl-4">236                    <h3 className="text-sm font-semibold">{m.title}</h3>237                    <p className="mt-1 text-sm leading-relaxed text-muted-foreground">238                      {m.description}239                    </p>240                  </div>241                </Reveal>242              ))}243            </div>244          </section>245        )}246      </div>247248      {/* Reproducibility */}249      {detail.reproducibility.length > 0 && (250        <section className="mt-14">251          <Reveal>252            <SectionHeading title="Reproducibility" icon={Github} />253          </Reveal>254          <ul className="mt-5 grid gap-3 sm:grid-cols-2">255            {detail.reproducibility.map((item, i) => (256              <Reveal key={item} delay={i * 0.05} className="h-full">257                <li className="flex h-full items-start gap-3 border border-border bg-card px-4 py-3.5 shadow-[var(--shadow-soft)]">258                  <CheckCircle2259                    className="mt-0.5 h-4 w-4 shrink-0 text-primary"260                    aria-hidden="true"261                  />262                  <span className="text-sm leading-relaxed text-foreground/85">263                    {item}264                  </span>265                </li>266              </Reveal>267            ))}268          </ul>269        </section>270      )}271272      {/* Keywords + CTA */}273      <Reveal className="mt-14">274        <footer className="border-t-2 border-foreground pt-6">275          <div className="flex flex-wrap gap-1.5">276            {detail.keywords.map((k) => (277              <Badge key={k} variant="outline">278                {k}279              </Badge>280            ))}281          </div>282          <div className="mt-6 flex flex-wrap gap-2">283            <a284              href={paper.pdf}285              target="_blank"286              rel="noopener noreferrer"287              className={buttonVariants({ size: "sm" })}288            >289              <FileText aria-hidden="true" />290              Read the paper (PDF)291            </a>292            {paper.repo && (293              <a294                href={paper.repo}295                target="_blank"296                rel="noopener noreferrer"297                className={buttonVariants({ variant: "outline", size: "sm" })}298              >299                <Github aria-hidden="true" />300                Explore the repository301              </a>302            )}303          </div>304        </footer>305      </Reveal>306    </article>307  );308}309